package webx

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"math/rand"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"sync"

	"gitee.com/xungen/goweb/errno"
)

type HostItem struct {
	Host   string `json:"host"`
	Port   int    `json:"port"`
	Weight int    `json:"weight"`
}

var routeport int
var routehost string
var routeversion string
var routerwmutex sync.RWMutex
var routehostmap = make(map[string][]HostItem)

func FormatPath(path string) string {
	return strings.ReplaceAll("/"+path, "//", "/")
}

func GetHttpEtag(response *http.Response) string {
	if response == nil {
		return ""
	}
	if response.StatusCode != 200 && response.StatusCode != 304 {
		return ""
	}
	return response.Header.Get("ETag")
}

func GetHttpStatus(response *http.Response) int {
	if response == nil {
		return 0
	}
	return response.StatusCode
}

func GetLink(host string, port int, path string, crypted ...bool) string {
	if len(path) > 0 {
		if path[0] == '/' {
			path = path[1:]
		}
	}

	if port == 443 && len(crypted) == 0 {
		crypted = []bool{true}
	}

	if len(crypted) > 0 && crypted[0] {
		res := "https://" + host
		if port == 443 {
			return res + "/" + path
		}
		return fmt.Sprintf("%s:%d/%s", res, port, path)

	} else {
		res := "http://" + host
		if port == 80 {
			return res + "/" + path
		}
		return fmt.Sprintf("%s:%d/%s", res, port, path)
	}
}

func GetRouteHost() *HostItem {
	routerwmutex.RLock()
	defer routerwmutex.RUnlock()
	item := &HostItem{Host: routehost, Port: routeport}
	if item.Host == "" || item.Port <= 0 {
		return nil
	} else {
		return item
	}
}

func GetHost(path string) *HostItem {
	path = FormatPath(path)
	routerwmutex.RLock()
	defer routerwmutex.RUnlock()
	list, ok := routehostmap[path]
	if ok {
		item := list[rand.Intn(len(list))]
		return &HostItem{Host: item.Host, Port: item.Port, Weight: item.Weight}
	}
	return nil
}

func updateRoute(host string, port int) {
	defer func() {
		if err := recover(); err == nil {
			slog.Info("update route list success")
		} else {
			slog.Error("update route list error", errno.Attr(err))
		}
	}()

	type PathItem struct {
		Path string     `json:"path"`
		List []HostItem `json:"list"`
	}

	type Result struct {
		Log  LogConfig  `json:"log"`
		List []PathItem `json:"list"`
	}

	cfg := GetConfig()
	link := GetLink(host, port, "exportroute")
	params := map[string]string{"access": "1"}
	header := map[string]string{"If-None-Match": routeversion}

	if cfg.App.Route.Port > 0 {
		params["clientid"] = strconv.Itoa(cfg.App.Id)
		params["clientname"] = cfg.App.Name
		params["clientport"] = strconv.Itoa(cfg.App.Port)
	}

	res := errno.AssertNoError(getHttpResult(link, params, header))
	buff := errno.AssertNoError(readFromHttpResponse(res))
	version := GetHttpEtag(res)
	if version == "" || version == routeversion {
		return
	}

	data := Result{}
	json.Unmarshal(buff, &data)

	num := len(data.List)
	hostmap := make(map[string][]HostItem)
	if num > 0 {
		for i := 0; i < num; i++ {
			pitem := data.List[i]
			hostlist := make([]HostItem, 0, 5)
			for j := 0; j < len(pitem.List); j++ {
				hitem := pitem.List[j]
				if hitem.Weight < 1 {
					hitem.Weight = 1
				} else if hitem.Weight > 9 {
					hitem.Weight = 9
				}
				for k := 0; k < hitem.Weight; k++ {
					hostlist = append(hostlist, hitem)
				}
			}
			hostmap[FormatPath(pitem.Path)] = hostlist
		}
	}

	updateRouteHost(host, port, version, hostmap)
	tryUpdateConfig(&data.Log)
}

func GetHttpResult(ctx context.Context, url string, params ...map[string]string) []byte {
	res, err := getHttpResult(url, params...)
	if err != nil {
		slog.ErrorContext(ctx, "get http result error", errno.Attr(err), slog.Any("url", url))
		return nil
	}
	buff, err := readFromHttpResponse(res)
	if err != nil {
		slog.ErrorContext(ctx, "read http response error", errno.Attr(err), slog.Any("url", url))
		return nil
	}
	return buff
}

func GetRemoteResult(path string, params ...map[string]string) (*http.Response, error) {
	item := GetHost(path)
	if item == nil {
		errno.SYSERR.Panic("resource[" + path + "] not found")
	}
	link := GetLink(item.Host, item.Port, path)
	return getHttpResult(link, params...)
}

func readFromHttpResponse(response *http.Response) ([]byte, error) {
	defer response.Body.Close()
	return io.ReadAll(response.Body)
}

func updateRouteHost(host string, port int, etag string, hostmap map[string][]HostItem) {
	routerwmutex.Lock()
	defer routerwmutex.Unlock()
	routehost, routeport, routeversion, routehostmap = host, port, etag, hostmap
}

func getHttpResult(link string, params ...map[string]string) (*http.Response, error) {
	num := len(params)
	method := http.MethodGet
	if num > 0 {
		method = http.MethodPost
	}

	var buffer *bytes.Buffer
	var request *http.Request

	if num > 0 {
		data := url.Values{}
		for k, v := range params[0] {
			data.Add(k, v)
		}
		buffer = bytes.NewBuffer([]byte(data.Encode()))
	} else {
		buffer = &bytes.Buffer{}
	}

	request, err := http.NewRequest(method, link, buffer)
	if err != nil {
		return nil, err
	}

	if num > 1 {
		for k, v := range params[1] {
			request.Header.Add(k, v)
		}
	}

	return http.DefaultClient.Do(request)
}
